6. 多线程

    内容包括
    >> code/mt
    >> code/multithread
    >> 共享变量问题

在开始之前我们先了解接个概念,出于理解的目的,都属于口头定义。

  • 程序:一堆代码以文本形式存入一个文档,可以被解释或者编译后让计算机去执行。

  • 进程:

    • 是程序运行的一个状态

    • 因为程序在运行,所以包含程序运行的时候的地址空间,内存,数据栈等

    • 每个进程由自己完全独立的运行环境

    • 多进程共享数据是一个问题

    进程可以看做是你打开某一个程序后的运行状态,比如打开QQ那就启动了一个QQ进程,如果再打开一个QQ,你虽然 电脑上运行了两个QQ,但这两个QQ之间的运行数据其实不共享,你登录的也是两个号码。

  • 线程:

    • 把一个进程,也就是一个程序的的运行细分成几个片段,每一个片段理解成一个线程,比如我教python是一个 进程的话,那么备课,上课,讲作业,答疑可以看做教python这件事的子任务,也可以理解成都是教python这个进程的线程。

    • 一个进程的多个线程间共享共享同一套数据和上下文运行环境

    • 因为贡献共同一套资源,可能会存在资源的共享互斥问题,比如我备课要用电脑,上课也要用电脑,如果我一边上课 一边备课的话,我的电脑到底应该打开上课的课件还是出现备课的内容?

  • 全局解释器锁(GIL:GlobalInterpreterLock):

    • Python开发的时候为了彻底解决多线程共享资源的问题,cPython解释器禁止真正的多线程,即一个CPU内不允许同时 运行两个线程,注意是同时,这个机制成为GIL

  • 举一个栗子:

    • 把程序运行当成做饭就好了,做一桌饭就是一个进程, 一般只有一个,必须要同时准备两桌饭的情况少

    • 做饭的妈妈就是CPU,在做饭过程中,红烧肉,炒白菜,炖肘子就是三个线程

    • 效率最低的就是做好红烧肉再炒白菜,炒好白菜在准备做炖肘子,这样其实三个线程每个执行完后再执行,等于不分线程,即单线程

    • 在做饭过程中我们使用同一套资源,比如案板,菜刀,锅等,还有同一个妈妈

    • 妈妈都是超人,她的正常顺序是红烧肉放上水炖着就收拾肘子,红烧肉好了把肘子炖上后开始洗菜切菜,肘子好了直接开炒白菜

    • 如果按照方案2做的饭时间缩短好多,前提是提高了资源(厨房用具)的利用率和妈妈的或火气值(CPU的温度,CPU利用率)

    • 如果很着急,可以让爸爸也进厨房帮忙(增加一个CPU,多核), 爸爸炒菜做红烧肉,妈妈炖肘子

    • 一般程序没有GIL,所以爸爸妈妈可以同时在厨房干活,但Python这个厨房有GIL锁,规定一个时间厨房里只能有一个人,这样 如果一定要让爸爸帮忙,只能妈妈出来爸爸再进去(不是真正的多核多线程)

多线程很多时候我们会感觉是这个程序好几个任务在”同时”执行, 但其实是CPU利用自己超高的执行速度把时间片段 分化的很细,每个任务轮流去执行,只不过轮换的速度极快,你不会感觉到卡顿。

6.1. thread包实现多线程

thread这个模块可以实现多线程,因为历史原因,thread也有些问题,所以并不推荐,但这个模块对 多线程的使用比较底层,python3中把这个模块改成了_thread

下面我用代码展示thread模块的时候用。

6.1.1. 顺序执行案例

time模块中有个函数ctime可以用来得到当前的时间,我们程序每次执行开头和结束得到这个时间,然后 就可以大致测量一个函数的执行时间。

看下面代码:

'''
利用time函数,生成两个函数
顺序调用
计算总的运行时间
'''
import time

def loop1():
    print('Start loop 1 at :', time.ctime())
    time.sleep(4)
    print('End loop 1 at:', time.ctime())

def loop2():
    print('Start loop 2 at :', time.ctime())
    time.sleep(2)
    print('End loop 2 at:', time.ctime())

def main():
    print("Starting at:", time.ctime())
    loop1()
    loop2()
    print("All done at:", time.ctime())

if __name__ == '__main__':
    main()

函数loop1loop2是顺序执行,程序总执行时间也基本等于这两个函数时间的和。

程序执行结果:

Starting at: Tue Apr 27 14:41:56 2021
Start loop 1 at : Tue Apr 27 14:41:56 2021
End loop 1 at: Tue Apr 27 14:42:00 2021
Start loop 2 at : Tue Apr 27 14:42:00 2021
End loop 2 at: Tue Apr 27 14:42:02 2021
All done at: Tue Apr 27 14:42:02 2021

6.1.2. 第一个多线程案例

如果我们把这两个函数改成度线程,则在这个程序执行中,这是一个进程,但在进程里有两个线程,线程是 并行执行的,所以程序总执行时间应该大体等于这两个函数执行时间最长的时间。

如果使用_thread创建多线程,则需要用函数start_new_thread创建一个县城并执行, 这个函数的参数有两个,第一个是多线程中需要执行的任务,是一个函数,需要注意的是调用格式,即只写入 函数的名称即可, 第二个参数是元祖,用来 表示执行函数的时候需要的参数,如果调用函数不需要参数,则写空元祖。

在需要注意的是,我们在下面的代码中创建了两个线程,这两个线程叫子线程,那么下面代码的执行,其实也是 一个线程,我们叫主线程,简而言之,是主线程创建了子线程,创建完子线程后主线程如果执行完毕,可能造成一些问题, 所以在代码中为了不让主线程结束执行,我们给加了一个死循环。

我们把上面程序改造成度线程,多线程利用_thread模块实现:

'''
利用time函数,生成两个函数
顺序调用
计算总的运行时间
'''
import time
import _thread as thread

def loop1():
    print('Start loop 1 at :', time.ctime())
    time.sleep(4)
    print('End loop 1 at:', time.ctime())

def loop2():
    print('Start loop 2 at :', time.ctime())
    time.sleep(2)
    print('End loop 2 at:', time.ctime())

def main():
    print("Starting at:", time.ctime())
    # 启动多线程的意思是用多线程去执行某个函数
    # 启动多线程函数为start_new_thead
    # 参数两个,一个是需要运行的函数名,第二是函数的参数,作为元祖使用,为空则使用空元祖
    # 注意:如果函数只有一个参数,需要参数后由一个逗号
    
    # 线程开始创建后开始执行
    thread.start_new_thread(loop1, ())
    thread.start_new_thread(loop2, ())

    print("All done at:", time.ctime())


if __name__ == '__main__':
    main()
    # 下面这个死循环要有,否则主线程结束后得不到我们的结果
    while True:
        time.sleep(1)

上面代码的执行结果如下:

Starting at: Tue Apr 27 14:47:57 2021
All done at: Tue Apr 27 14:47:57 2021
Start loop 1 at :Start loop 2 at : Tue Apr 27 14:47:57 2021
Tue Apr 27 14:47:57 2021
End loop 2 at: Tue Apr 27 14:47:59 2021
End loop 1 at: Tue Apr 27 14:48:01 2021

上面代码子线程loop1loop2执行时间分别是4秒和2秒,而主线程只是启动了两个线程然后就结束了, 理论上执行时间可以忽略掉,所以,这个程序总共执行时间基本上是 max(4,2)

6.1.3. 子线程带参数调用

下面案例展示了一个启动一个需要参数的子线程的案例,其余的都一样,不做过多解释。

# 练习带参数的多线程启动方法
import time
import _thread as thread

def loop1(in1):
    print('Start loop 1 at :', time.ctime())
    print("我是参数 ",in1)
    time.sleep(4)
    print('End loop 1 at:', time.ctime())

def loop2(in1, in2):
    print('Start loop 2 at :', time.ctime())
    print("我是参数 " ,in1 , "和参数  ", in2)
    time.sleep(2)
    print('End loop 2 at:', time.ctime())

def main():
    print("Starting at:", time.ctime())
    # 启动多线程的意思是用多线程去执行某个函数
    # 启动多线程函数为start_new_thead
    # 参数两个,一个是需要运行的函数名,第二是函数的参数作为元祖使用,为空则使用空元祖
    # 注意:如果函数只有一个参数,需要参数后由一个逗号
    thread.start_new_thread(loop1,("王老大", ))
    thread.start_new_thread(loop2,("王大鹏", "王晓鹏"))

    print("All done at:", time.ctime())

if __name__ == "__main__":
    main()
    while True:
        time.sleep(10)

因为_thread已不再推荐使用,所以我们不在深入讲解,下面讲解常用的多线程调用方法。

6.2. threading的使用

threading是我们多线程代码中常用到的模块,我们在本章中进行介绍。

6.2.1. threading的基本使用

threading模块使用多线程的过程一般分为这两步:

  • 直接利用threading.Thread生成Thread实例

      t = threading.Thread(target=xxx, args=(xxx,))
    
  • 启动生成的实例去执行

       # 启动多线程
       t.start()
       # 等待多线程执行完成
       t.join()
    

我们还是从一个案例开始, 其中threading.Thread实例生成至少需要两个参数:target和arg:

  • target:多线程的执行代码块,这里是一个函数

  • args:启动多线程代码的参数元祖

一旦生成Thread实例后,需要利用:

  • Thread.start: 启动多线程的执行

  • Thread.join: 告诉主线程等待多线程执行完毕

正是因为有了Thread.join函数,在主线程的代码中才不再需要死循环来强制主线程的执行时间长过子线程的执行。

代码如下,指向效果跟上面的代码一致:

import time
# 导入多线程处理包
import threading

def loop1(in1):
    print('Start loop 1 at :', time.ctime())
    print("我是参数 ",in1)
    time.sleep(4)
    print('End loop 1 at:', time.ctime())

def loop2(in1, in2):
    print('Start loop 2 at :', time.ctime())
    print("我是参数 " ,in1 , "和参数  ", in2)
    time.sleep(2)
    print('End loop 2 at:', time.ctime())

def main():
    print("Starting at:", time.ctime())
    # 生成threading.Thread实例
    t1 = threading.Thread(target=loop1, args=("王老大",))
    t1.start()

    t2 = threading.Thread(target=loop2, args=("王大鹏", "王小鹏"))
    t2.start()

    # 等待执行完毕
    t1.join()
    t2.join()

    print("All done at:", time.ctime())

if __name__ == "__main__":
    main()
    while True:
        time.sleep(10)

6.2.2. threaidng的守护线程daemon

守护线程就是看线程离开主线程后是否能独立运行:

  • 如果在程序中将子线程设置成守护现成,则子线程会在主线程结束的时候自动退出

  • 一般认为,守护线程不中要或者不允许离开主线程独立运行

  • 守护线程案例能否有效果跟环境相关

下面案例没有设置守护进程,则子线程在主线程结束后也能运行:

import time
import threading

def fun():
    print("Start fun")
    time.sleep(2)
    print("end fun")

print("Main thread")

t1 = threading.Thread(target=fun, args=() )
t1.start()

time.sleep(1)
print("Main thread end")

运行结果如下:

Main thread
Start fun
Main thread end
end fun

下面案例是一个守护进程的案例,注意已经设置了守护进程:

import time
import threading

def fun():
    print("Start fun")
    time.sleep(2)
    print("end fun")

print("Main thread")

t1 = threading.Thread(target=fun, args=() )
t1.setDaemon(True)
t1.start()

time.sleep(1)
print("Main thread end")

执行后结果如下:

Main thread
Start fun
Main thread end

除此之外线程还有一些常见的属性,在实际使用中可以利用:

  • threading.currentThread:返回当前线程变量

  • threading.enumerate:返回一个包含正在运行的线程的list,正在运行的线程指的是线程启动后,结束前的状态

  • threading.activeCount: 返回正在运行的线程数量,效果跟 len(threading.enumerate)相同

  • thr.setName: 给线程设置名字

  • thr.getName: 得到线程的名字

    import time import threading

    def loop1(): # ctime 得到当前时间 print(‘Start loop 1 at :’, time.ctime()) # 睡眠多长时间,单位是秒 time.sleep(6) print(‘End loop 1 at:’, time.ctime())

    def loop2(): # ctime 得到当前时间 print(‘Start loop 2 at :’, time.ctime()) # 睡眠多长时间,单位是秒 time.sleep(1) print(‘End loop 2 at:’, time.ctime())

    def loop3(): # ctime 得到当前时间 print(‘Start loop 3 at :’, time.ctime()) # 睡眠多长时间,单位是秒 time.sleep(5) print(‘End loop 3 at:’, time.ctime())

    def main(): print(“Starting at:”, time.ctime()) # 生成threading.Thread实例 t1 = threading.Thread(target=loop1, args=( )) # setName是给每一个子线程设置一个名字 t1.setName(“THR_1”) t1.start()

      t2 = threading.Thread(target=loop2, args=( ))
      t2.setName("THR_2")
      t2.start()
    
      t3 = threading.Thread(target=loop3, args=( ))
      t3.setName("THR_3")
      t3.start()
    
      # 预期3秒后,thread2已经自动结束,
      time.sleep(3)
      # enumerate 得到正在运行子线程,即子线程1和子线程3
      for thr in threading.enumerate():
          # getName能够得到线程的名字
          print("正在运行的线程名字是: {0}".format(thr.getName()))
    
      print("正在运行的子线程数量为: {0}".format(threading.activeCount()))
    
      print("All done at:", time.ctime())
    

    if name == “main”: main() # 一定要有while语句 # 因为启动多线程后本程序就作为主线程存在 # 如果主线程执行完毕,则子线程可能也需要终止 while True: time.sleep(10)

执行结果如下:

('Starting at:', 'Tue May 11 15:11:36 2021')
('Start loop 2 at :', 'Tue May 11 15:11:36 2021'()
'Start loop 3 at :' , ('Tue May 11 15:11:36 2021''Start loop 1 at :')
, 'Tue May 11 15:11:36 2021')
('End loop 2 at:', 'Tue May 11 15:11:37 2021')
正在运行的线程名字是: MainThread
正在运行的线程名字是: pydevd.Writer
正在运行的线程名字是: pydevd.Reader
正在运行的线程名字是: pydevd.CommandThread
正在运行的线程名字是: THR_3
正在运行的线程名字是: THR_1
正在运行的子线程数量为: 6
('All done at:', 'Tue May 11 15:11:39 2021')
('End loop 3 at:', 'Tue May 11 15:11:41 2021')
('End loop 1 at:', 'Tue May 11 15:11:42 2021')

6.2.3. threading的面向对象写法

有时候线程可以时候用面向对象技术,这样会最大程度利用OOP的好处,特别是子线程需要处理的内容比较复杂的时候。 使用方法可以直接继承threading.Thread,这时候只要重写run函数就可以,或者也可以 把子线程携程一个类,然后调用类的实例的方法。

下面案例直接重写threading.run,继承threading.Thread:

import threading
import time

# 1. 类需要继承自threading.Thread
class MyThread(threading.Thread):
    def __init__(self, arg):
        super(MyThread, self).__init__()
        self.arg = arg

    # 2 必须重写run函数,run函数代表的是真正执行的功能
    def  run(self):
        time.sleep(2)
        print("The args for this class is {0}".format(self.arg))

for i in range(5):
    t = MyThread(i)
    t.start()
    t.join()

print("Main thread is done!!!!!!!!")

下面这个案例的写法比较成熟:

#coding=utf-8
import threading
from time import sleep, ctime

loop = [4,2]

class ThreadFunc:

    def __init__(self, name):
        self.name = name

    def loop(self, nloop, nsec):
        '''
        :param nloop: loop函数的名称
        :param nsec: 系统休眠时间
        :return:
        '''
        print('Start loop ', nloop, 'at ', ctime())
        sleep(nsec)
        print('Done loop ', nloop, ' at ', ctime())

def main():
    print("Starting at: ", ctime())

    # ThreadFunc("loop").loop 跟一下两个式子相等:
    # t = ThreadFunc("loop")
    # t.loop
    # 以下t1 和  t2的定义方式相等
    t = ThreadFunc("loop")
    t1 = threading.Thread( target = t.loop, args=("LOOP1", 4))
    # 下面这种写法更西方人,工业化一点
    t2 = threading.Thread( target = ThreadFunc('loop').loop, args=("LOOP2", 2))

    # 常见错误写法
    #t1 = threading.Thread(target=ThreadFunc('loop').loop(100,4))
    #t2 = threading.Thread(target=ThreadFunc('loop').loop(100,2))

    t1.start()
    t2.start()

    t1.join( )
    t2.join()


    print("ALL done at: ", ctime())


if __name__ == '__main__':
    main()

6.3. 多线程变量共享问题

        - 共享变量: 当多个现成同时访问一个变量的时候,会产生共享变量的问题
        - 案例11
        - 解决变量:锁,信号灯,
        - 锁(Lock):
            - 是一个标志,表示一个线程在占用一些资源
            - 使用方法
                - 上锁
                - 使用共享资源,放心的用
                - 取消锁,释放锁
            - 案例12
            - 锁谁: 哪个资源需要多个线程共享,锁哪个
            - 理解锁:锁其实不是锁住谁,而是一个令牌
        - 线程安全问题:
            - 如果一个资源/变量,他对于多线程来讲,不用加锁也不会引起任何问题,则称为线程安全
            - 线程不安全变量类型: list, set, dict
            - 线程安全变量类型: queue
        - 生产者消费者问题, v13
            - 一个模型,可以用来搭建消息队列, 
            - queue是一个用来存放变量的数据结构,特点是先进先出,内部元素排队,可以理解成一个特殊的list
        - 死锁问题, 案例14
        - 锁的等待时间问题, v15
        - semphore
            - 允许一个资源最多由几个多线程同时使用
            - v16
        - threading.Timer
            - 案例 17
            - Timer是利用多线程,在指定时间后启动一个功能
            
        - 可重入锁
            - 一个锁,可以被一个线程多次申请
            - 主要解决递归调用的时候,需要申请锁的情况
            - 案例18
            
            
            
                
                # 线程替代方案
                -  subprocess
                    - 完全跳过线程,使用进程
                    - 是派生进程的主要替代方案
                    - python2.4后引入
                - multiprocessiong
                    - 使用threadiing借口派生,使用子进程
                    - 允许为多核或者多cpu派生进程,接口跟threading非常相似
                    - python2.6
                    
                - concurrent.futures
                    - 新的异步执行模块
                    - 任务级别的操作
                    - python3.2后引入
                # 多进程
                - 进程间通讯(InterprocessCommunication, IPC )
                - 进程之间无任何共享状态
                - 进程的创建
                    - 直接生成Process实例对象, 案例19
                    - 派生子类, 案例20
                    
                - 在os中查看pid,ppid以及他们的关系              
                    - 案例21
                - 生产者消费者模型
                    - JoinableQueue
                    - 案例22
                    - 队列中哨兵的使用, 案例23 
                    - 哨兵的改进, 案例24
                # 0.环境
                - XUbuntu16.04
                - anaconda
                - pycharm
                - python3.6
                - conda create -n xdl python=3.6
                # 1. 参考资料
                - [GIL_01](http://blog.csdn.net/bitcarmanlee/article/details/51577014)
                - [GIL_02](https://www.cnblogs.com/menkeyi/p/5806601.html)
                - [详解python中的多线程编程](http://www.jb51.net/article/63784.htm)
                - [GDB多线程调试](https://www.cnblogs.com/jingzhishen/p/4324071.html)
                - [三种多线程方法](http://blog.csdn.net/smalltankpy/article/details/71698656)
                - [多线程](https://www.cnblogs.com/yeayee/p/4952022.html)
                - [多进程官网](https://docs.python.org/2/library/multiprocessing.html#sharing-state-between-processes)
                - [知乎-pyhton多线程是不是鸡肋](https://www.zhihu.com/question/23474039)
                - [python多线程那点事](http://blog.51reboot.com/p534/)
                - [python-GIL](http://blog.51reboot.com/global-interpretor-lock/)
                - [生产者消费者模型](http://www.cnblogs.com/bizhu/archive/2012/05/17/2506202.html)
                - [死锁-哲学家问题](http://blog.sina.com.cn/s/blog_c33b15000102x6qh.html)
                - [python多线程同步的四种方式](http://www.jb51.net/article/112711.htm)
                - [Python多线程详解](http://python.jobbole.com/85050/)
                - [python的with语句](http://blog.csdn.net/yxwb1253587469/article/details/52248565)
                # 2.多线程 vs 多进程
                - 进程:
                    - 程序就是存储在硬盘上的可执行的一个文件
                    - 一个程序的一次执行就是启动了一个进程
                    - 进程包含地址空间,内存,数据栈,其他数据
                    - 进程可以派生新的进程
                    - 每个进程拥有自己完全独立的运行环境,多进程间数据共享是个难题
                - 线程:
                    - 一个进程的独立运行片段,一个进程可以拥有多个线程
                    - 轻量化进程
                    - 一个进程的多线程间共享数据和上下文运行环境
                    - 共享互斥问题
                - 全局解释器锁(GIL)
                    - Python代码的执行是由Python虚拟机进行控制
                    - 在主循环中同事只能由一个控制线程在执行
                    - Python解释器可以运行多个线程,但在任意一个时间点,只能由一个线程被解释器执行
                    - 案例01_gil.py可以说明gil的存在
                    
                # 3.多线程
                - python包支持
                    - thread: 有问题,不好用,在python3里边改成了_thread
                    - threading:推荐使用
                - 调试问题
                    - 多线程调试建议在单线程运行状态下先行调试
                    
                - 案例 01.py:
                    - 体会顺序执行的总时间
                - 多线程创建
                    - 利用_thread创建
                        - start_new_thread
                      - 案例02.py
                      - 案例03.py
                         - 调用带参数的函数,让他用多线程执行
                    - 直接利用threading.Thread生成Thread实例
                        - 代码
                          ```
                              t = threading.Thread(target=xxx, args=(xxx,))
                              t.start()
                        - 案例04.py, 修改案例03为此方法执行
                    - 创建threading.Thread 的子类启动
                        - 案例09.py,修改自案例03
                    - threading.join
                        - 代表线程不往下执行,等待此线程执行完毕才继续往下执行
                        - 案例05.py:修改案例04.py,加入等待,直到子线程loop1和loop2执行完毕后主线程才继续
                    
                    - 守护线程 - daemon
                         - 如果在程序中将子线程设置为守护线程,则该子线程会在主线程结束后自动退出
                         - 一般认为,子线程不重要或者不允许离开主线程独立运行时采用
                         - 案例06.py为非守护线程, 07.py为守护线程   
                - 线程常用属性
                   - threading.currentThread:返回当前线程变量
                   - threading.enumerate:返回一个包含正在运行的线程的list,正在运行的线程指的是线程启动后,结束前的状态
                   - threading.activeCount: 返回正在运行的线程数量,效果跟 len(threading.enumerate)相同
                   - thr.setName: 给线程设置名字
                   - thr.getName: 得到线程的名字
                   - 案例08.py       
                - 案例10.py, 成熟风格写法

                # 4. 多线程共享变量问题 
                - 变量互斥问题案例001.py
                - 线程安全数据
                    - 非安全
                    - 安全
                - 常用技术
                    - Lock
                    - RLock
                    - Semaphore
                    - Event
                - 共享问题
                    - lock概念
                    - lock的使用方法
                    - 案例002
                - 生产者消费者模型
                    - 案例003
                - 哲学家问题-死锁
                    - 死锁案例 004
                    - 问题解决:
                        - 无解
                        - 加timeout参数
                        - 案例 005 
                - 其他控制变量
                    - Semaphore
                        - 控制可入数量
                        - 案例006
                    - Event
                        - 内部有标志位,初始为false
                        - 使用set来是标志位变为true
                        - clear可以重新设置标志位为false
                        - is_set()可以检查标志位状态
                        - wait(timeout=None)用来阻塞当前线程,直到标志位复位或者timeout
                        - 案例007
                - 线程局部变量
                    - myLocal = threading.local
                - with的使用
                - local和with参看案例008
                - Timer
                    - 一个特殊的线程
                    - 案例009